import tkinter as tk, copy, math
from tkinter import messagebox, filedialog
from bresenham import bresenham as bresenham_line

from strange_io.pixels import StrangePixels
from strange_io.colors import StrangeColors
from protolib import ObjectBuffer, rgb_to_hex, get_app_root

from DoubleScrolledFrame import DoubleScrolledFrame
from algs.bresenham_circle import generate_circle_coordinates as bresenham_circle
from algs.flood import floodFill

class PixelsEditor(tk.Frame):
    def __init__(self, parent, controller, app_config, canvas_width=20, canvas_height=20, pixel_size=20):
        super().__init__(parent)
        self.menu_bar = tk.Menu(controller)

        #region vars
        #region
        self.current_file = tk.StringVar(self)
        self.current_palette_file = tk.StringVar(self)
        self.is_edited = tk.BooleanVar(self)
        self.color = tk.IntVar(self)
        self.brush = tk.StringVar(self)
        if app_config['canvas']['brush']['default'] in ['none', 'brush', 'erase', 'line', 'circle', 'flood']:
            self.brush.set(app_config['canvas']['brush']['default'])
        else:
            print(f"WARNING: Invalid brush: {app_config['canvas']['brush']['default']}")
            self.brush.set('none')
        #endregion

        #region
        self.canvas_width = tk.IntVar(self)
        self.canvas_width.set(canvas_width)
        self.canvas_height = tk.IntVar(self)
        self.canvas_height.set(canvas_height)
        self.pixel_size = tk.IntVar(self)
        self.pixel_size.set(pixel_size)
        #endregion
        #endregion

        #region menu bar
        #region file
        self.file_menu = tk.Menu(self.menu_bar, tearoff=0)
        self.menu_bar.add_cascade(label="File", menu=self.file_menu)
        self.file_menu.add_command(label="New", command=self.new_file)
        self.file_menu.add_command(label="Open", command=self.load_file)
        self.file_menu.add_command(label="Save", command=self.save_file)
        self.file_menu.add_command(label="Export", command=self.export_file)
        self.file_menu.add_command(label="Reload", command=self.reload_file)
        self.file_menu.add_separator()
        self.file_menu.add_command(label="Undo", command=self.undo_last_change)
        self.file_menu.add_separator()
        self.file_menu.add_command(label="Exit", command=controller.on_closing)
        #endregion
        #region palette
        self.palette_menu = tk.Menu(self.menu_bar, tearoff=0)
        self.menu_bar.add_cascade(label="Palette", menu=self.palette_menu)
        self.palette_menu.add_command(label="Load", command=self.load_palette)
        self.palette_menu.add_command(label="Reload", command=self.reload_palette)
        #endregion
        #region canvas
        self.canvas_menu = tk.Menu(self.menu_bar, tearoff=0)
        self.menu_bar.add_cascade(label="Canvas", menu=self.canvas_menu)
        self.canvas_menu.add_command(label="Refresh", command=self.refresh_canvas)
        self.canvas_menu.add_command(label="Restart", command=self.soft_restart_canvas)
        #endregion
        #region tool
        self.tool_menu = tk.Menu(self.canvas_menu)
        self.menu_bar.add_cascade(label="Tool", menu=self.tool_menu)
        self.tool_menu.add_radiobutton(label="None", variable=self.brush, value='none')
        self.tool_menu.add_radiobutton(label="Brush", variable=self.brush, value='brush')
        self.tool_menu.add_radiobutton(label="Erase", variable=self.brush, value='erase')
        self.tool_menu.add_radiobutton(label="Line", variable=self.brush, value='line')
        self.tool_menu.add_radiobutton(label="Circle", variable=self.brush, value='circle')
        self.tool_menu.add_radiobutton(label="Flood", variable=self.brush, value='flood')
        #endregion
        #region color
        self.color_menu = tk.Menu(self.menu_bar)
        self.menu_bar.add_cascade(label="Color", menu=self.color_menu)
        #endregion
        #endregion

        #region palette
        palette: list[tuple[int,int,int]] = [
            (0,0,0),
            (255,255,255)
        ]
        transperant = len(palette) - 1
        self.colors = StrangeColors(palette, transperant)
        #endregion

        self.spixels = StrangePixels(self.canvas_width.get(), self.canvas_height.get(), len(palette))
        self.buffer = ObjectBuffer()

        #region Elements
        #region Canvas
        self.outline_color = rgb_to_hex(app_config['canvas']['pixel']['border']['color'])
        self.outline_width = app_config['canvas']['pixel']['border']['width']
        self.canvas_frame = DoubleScrolledFrame(self, width=(self.canvas_width.get()*self.pixel_size.get())+24, height=(self.canvas_height.get()*self.pixel_size.get())+24)
        self.canvas = tk.Canvas(self.canvas_frame, width=(self.canvas_width.get()*self.pixel_size.get())+3, height=(self.canvas_height.get()*self.pixel_size.get())+3)
        self.save_button = tk.Button(self, text='Save', command=self.save_file)
        self.hard_restart_canvas()
        #endregion

        controller.bind("<Control-s>", self.on_control_s)
        controller.bind("<Control-z>", self.on_control_z)
        controller.bind("<n>", self.on_n)
        controller.bind("<e>", self.on_e)
        controller.bind("<b>", self.on_b)
        controller.bind("<f>", self.on_f)
        controller.bind("<l>", self.on_l)
        controller.bind("<c>", self.on_c)

        pixels = copy.deepcopy(self.pixel_indexes)
        self.buffer.add_state(pixels)

        self.brush_buffer:tuple[int,int]=(0,0)

        self.refresh_color_menu()

    def update_pixel_color(self, x, y, color: int):
        if x < 0 or y < 0:
            raise IndexError('Invalid pixel coordinates')
        self.canvas.itemconfig(self.pixels[x][y], fill=rgb_to_hex(self.colors.colors[color]))
        self.pixel_indexes[x][y] = color
        self.is_edited.set(True)

    def get_strange_pixels(self):
        strange_pixels = {}
        strange_pixels['palette_size'] = len(self.colors.colors)
        strange_pixels['width'] = self.canvas_width.get()
        strange_pixels['height'] = self.canvas_height.get()
        strange_pixels['image'] = self.pixel_indexes
        return strange_pixels

    def new_file(self):
        window = tk.Toplevel()
        window.resizable(0,0)
        window.title('New file')

        def create_callback():
            width = width_entry.get()
            if width == '':
                messagebox.showerror(messagebox.ERROR, 'Document width not specified')
                return
            try:
                int(width)
            except ValueError as e:
                print(e.__class__.__name__ + ':', e)
                messagebox.showerror(messagebox.ERROR, 'Width must be a number')
                return
            height = height_entry.get()
            if height == '':
                messagebox.showerror(messagebox.ERROR, 'Document height not specified')
                return
            try:
                int(height)
            except ValueError as e:
                print(e.__class__.__name__ + ':', e)
                messagebox.showerror(messagebox.ERROR, 'Height must be a number')
                return
            self.canvas_width.set(width)
            self.canvas_height.set(height)
            self.current_file.set('')
            self.is_edited.set(False)
            self.hard_restart_canvas()
            self.buffer = ObjectBuffer()
            pixels = copy.deepcopy(self.pixel_indexes)
            self.buffer.add_state(pixels)
            window.destroy()

        #region document width
        width_frame = tk.Frame(window)
        width_frame.pack(fill=tk.X)
        width_label = tk.Label(width_frame, text='Width:')
        width_label.pack(side='left')
        width_entry = tk.Entry(width_frame)
        width_entry.insert(0, str(self.canvas_width.get()))
        width_entry.pack(side='right')
        #endregion

        #region document height
        height_frame = tk.Frame(window)
        height_frame.pack(fill=tk.X)
        height_label = tk.Label(height_frame, text='Height:')
        height_label.pack(side='left')
        height_entry = tk.Entry(height_frame)
        height_entry.insert(0, str(self.canvas_height.get()))
        height_entry.pack(side='right')
        #endregion

        create_button = tk.Button(window, command=create_callback, text='Create')
        create_button.pack(fill=tk.X)

    def load_file(self):
        filename = filedialog.askopenfilename(defaultextension=".spixels", filetypes=[("Strange Pixels", "*.spixels"), ("Strange I/O Documents", ("*.strange", "*.sio"))])
        if filename:
            self._open_file(filename)

    def _open_file(self, filepath):
        try:
            self.current_file.set(filepath)
            self.spixels.load_file(self.current_file.get())
            content = self.spixels.dump()
            self.canvas_width.set(content['width'])
            self.canvas_height.set(content['height'])
            self.hard_restart_canvas()
            self.pixel_indexes = content['image']
            self.refresh_canvas()
        except ValueError as e:
            messagebox.showerror('ValueError', str(e))
        self.is_edited.set(False)

    def reload_file(self):
        if self.current_file.get() != '':
            try:
                self.spixels.load_file(self.current_file.get())
                content = self.spixels.dump()
                self.canvas_width.set(content['width'])
                self.canvas_height.set(content['height'])
                self.hard_restart_canvas()
                self.pixel_indexes = content['image']
                self.refresh_canvas()
            except ValueError as e:
                messagebox.showerror('ValueError', str(e))
            self.is_edited.set(False)
        else:
            self.load_file()

    def save_file(self):
        if self.current_file.get() == '':
            file_path = filedialog.asksaveasfilename(defaultextension=".spixels", filetypes=[("Strange Pixels", "*.spixels"), ("Strange I/O Documents", ("*.strange", "*.sio")), ("All Files", "*.*")])
            if file_path:
                self.current_file.set(file_path)
            else:
                return
        try:
            self.spixels.load(self.get_strange_pixels())
            self.spixels.dump_file(self.current_file.get())
            messagebox.showinfo("Info", "File saved successfully!")
            self.is_edited.set(False)
        except ValueError as e:
            messagebox.showerror('ValueError', str(e))

    def load_palette(self):
        filename = filedialog.askopenfilename(defaultextension=".scolors", filetypes=[("Strange Colors Palettes", "*.scolors"), ("Strange I/O Documents", ("*.strange", "*.sio"))])
        if filename:
            try:
                self.current_palette_file.set(filename)
                self.colors.load_file(self.current_palette_file.get())
                self.refresh_color_menu()
                self.refresh_canvas()
            except ValueError as e:
                messagebox.showerror('ValueError', str(e))
            self.is_edited.set(False)

    def reload_palette(self):
        if self.current_palette_file.get() != '':
            try:
                self.colors.load_file(self.current_palette_file.get())
                self.refresh_color_menu()
                self.refresh_canvas()
            except ValueError as e:
                messagebox.showerror('ValueError', str(e))
            self.is_edited.set(False)
        else:
            self.load_palette()

    def export_file(self):
        file_path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("Portable Network Graphic", "*.png"), ("Joint Photographic Experts Group", ("*.jpeg", "*.jpg")), ("All Files", "*.*")])
        if file_path:
            try:
                pixels = self.get_strange_pixels()
                self.spixels.load(pixels)
                image = self.spixels.export(self.colors, True)
                image.save(file_path)
                image.close()
                messagebox.showinfo("Info", "File exported successfully!")
            except ValueError as e:
                messagebox.showerror('ValueError', str(e))

    def get_pixel(self, x, y):
        return (x - 6) // self.pixel_size.get(), (y - 6) // self.pixel_size.get()

    def canvas_click_callback(self, event):
        x, y = self.get_pixel(event.x, event.y)
        try:
            if self.brush.get() == 'none':
                pass
            elif self.brush.get() == 'brush':
                self.update_pixel_color(x, y, self.color.get())
            elif self.brush.get() == 'erase':
                self.update_pixel_color(x, y, self.colors.transperent_index)
            elif self.brush.get() == 'flood':
                self.flood_fill(x, y)
            elif self.brush.get() in ['line', 'circle']:
                self.brush_buffer = (x, y)
            else:
                raise ValueError(f'Unknown tool: {self.brush.get()}')
        except IndexError as e:
            print(e.__class__.__name__ + ': ' + str(e))

    def canvas_drag_callback(self, event):
        x, y = self.get_pixel(event.x, event.y)
        if self.brush.get() not in ['line', 'circle']:
            self.canvas_click_callback(event)
        elif self.brush.get() == 'line':
            self.pixel_indexes = copy.deepcopy(self.buffer.current_state)
            self.draw_line(self.brush_buffer, (x,y))
            self.refresh_canvas()
        elif self.brush.get() == 'circle':
            self.pixel_indexes = copy.deepcopy(self.buffer.current_state)
            self.draw_circle(self.brush_buffer, (x,y))
            self.refresh_canvas()

    def draw_line(self, start:tuple[int,int], end:tuple[int,int]):
        line = list(bresenham_line(*start, *end))
        for point in line:
            try:
                self.update_pixel_color(*point, self.color.get())
            except IndexError as e:
                print(e.__class__.__name__ + ':', str(e))

    def draw_circle(self, start:tuple[int,int], end:tuple[int,int]):
        d = int(math.sqrt(((end[0] - start[0]) ** 2) + ((end[1] - start[1]) ** 2)))
        circle = list(bresenham_circle(d))
        for point in circle:
            try:
                self.update_pixel_color(point[0] + start[0], point[1] + start[1], self.color.get())
            except IndexError as e:
                print(e.__class__.__name__ + ':', str(e))

    def flood_fill(self, x, y):
        screen = copy.deepcopy(self.pixel_indexes)
        m = len(screen)
        n = len(screen[0])
        prevC = screen[x][y]
        newC = self.color.get()
        floodFill(screen, m, n, x, y, prevC, newC)
        for i in range(n):
            for j in range(m):
                self.update_pixel_color(i, j, screen[i][j])

    def canvas_mouse_button_release_callback(self, event):
        x, y = self.get_pixel(event.x, event.y)
        if self.brush.get() == 'line':
            self.draw_line(self.brush_buffer, (x,y))
        elif self.brush.get() == 'circle':
            self.draw_circle(self.brush_buffer, (x,y))
        if self.brush.get() in ['brush', 'erase', 'line', 'circle', 'flood']:
            pixels = copy.deepcopy(self.pixel_indexes)
            self.buffer.add_state(pixels)

    def refresh_canvas(self):
        """
        Loop through pixel indexes, and reset pixel colors
        """

        is_edited = self.is_edited.get()
        for x, row in enumerate(self.pixel_indexes):
            for y, index in enumerate(row):
                self.update_pixel_color(x, y, index)
        self.is_edited.set(is_edited)

    def hard_restart_canvas(self):
        """
        Destroy canvas element, and create new one, and then initialization the new canvas as well as related data
        """

        if self.is_edited.get():
            if not messagebox.askokcancel('Warning', 'Restarting the canvas will clear everything, and you have not saved some changes.\nContinue?'):
                return
        self.save_button.destroy()
        self.canvas.destroy()
        self.canvas_frame.outer.destroy()
        del self.canvas_frame
        self.canvas_frame = DoubleScrolledFrame(self, width=(self.canvas_width.get() * (self.pixel_size.get() + self.outline_width)) + (self.canvas_width.get() * self.outline_width) + 24,
                                                      height=(self.canvas_height.get() * (self.pixel_size.get() + self.outline_width)) + (self.canvas_height.get() * self.outline_width) + 24)
        self.canvas_frame.pack(fill=tk.BOTH, expand=tk.YES)
        self.pixel_indexes = [[self.colors.transperent_index for _ in range(self.canvas_width.get())] for _ in range(self.canvas_height.get())]
        self.pixels = [[None for _ in range(self.canvas_width.get())] for _ in range(self.canvas_height.get())]
        self.canvas = tk.Canvas(self.canvas_frame, width=(self.canvas_width.get() * (self.pixel_size.get() + self.outline_width)) + 5 + self.outline_width,
                                                   height=(self.canvas_height.get() * (self.pixel_size.get() + self.outline_width)) + 5 + self.outline_width)
        self.canvas.bind("<Button-1>", self.canvas_click_callback)
        self.canvas.bind("<ButtonRelease-1>", self.canvas_mouse_button_release_callback)
        self.canvas.bind("<B1-Motion>", self.canvas_drag_callback)
        self.canvas.pack()
        self.save_button = tk.Button(self, text='Save', command=self.save_file)
        self.save_button.pack(fill=tk.X)
        for x in range(self.canvas_width.get()):
            for y in range(self.canvas_height.get()):
                self.pixels[x][y] = self.canvas.create_rectangle((x * (self.pixel_size.get() + self.outline_width)) + ((self.pixel_size.get() + self.outline_width) / 4) + 1,
                                                                 (y * (self.pixel_size.get() + self.outline_width)) + ((self.pixel_size.get() + self.outline_width) / 4) + 1,
                                                                 ((x + 1) * (self.pixel_size.get() + self.outline_width)) + ((self.pixel_size.get() + self.outline_width) / 4) + 1,
                                                                 ((y + 1) * (self.pixel_size.get() + self.outline_width)) + ((self.pixel_size.get() + self.outline_width) / 4) + 1,
                                                                 fill=rgb_to_hex(self.colors.colors[self.colors.transperent_index]), width=self.outline_width, outline=self.outline_color)
                # NOTE: kwarg outline sets the outline color

    def soft_restart_canvas(self):
        """
        Destroy canvas element, and create new one, and then initialization the new canvas as well
        """
        self.save_button.destroy()
        self.canvas.destroy()
        self.canvas_frame.outer.destroy()
        del self.canvas_frame
        self.canvas_frame = DoubleScrolledFrame(self, width=(self.canvas_width.get()*self.pixel_size.get())+24, height=(self.canvas_height.get()*self.pixel_size.get())+24)
        self.canvas_frame.pack(fill=tk.BOTH, expand=tk.YES)
        self.pixels = [[None for _ in range(self.canvas_width.get())] for _ in range(self.canvas_height.get())]
        self.canvas = tk.Canvas(self.canvas_frame, width=(self.canvas_width.get()*self.pixel_size.get())+3, height=(self.canvas_height.get()*self.pixel_size.get())+3, borderwidth=5)
        self.canvas.bind("<Button-1>", self.canvas_click_callback)
        self.canvas.bind("<ButtonRelease-1>", self.canvas_mouse_button_release_callback)
        self.canvas.bind("<B1-Motion>", self.canvas_drag_callback)
        self.canvas.pack()
        self.save_button = tk.Button(self, text='Save', command=self.save_file)
        self.save_button.pack(fill=tk.X)
        for x in range(self.canvas_width.get()):
            for y in range(self.canvas_height.get()):
                self.pixels[x][y] = self.canvas.create_rectangle((x * self.pixel_size.get()) + (self.pixel_size.get() / 4) + 2,
                                                                 (y * self.pixel_size.get()) + (self.pixel_size.get() / 4) + 2,
                                                                 ((x + 1) * self.pixel_size.get()) + (self.pixel_size.get() / 4) + 2,
                                                                 ((y + 1) * self.pixel_size.get()) + (self.pixel_size.get() / 4) + 2,
                                                                 fill=rgb_to_hex(self.colors.colors[self.colors.transperent_index]))
        self.refresh_canvas()

    def refresh_color_menu(self):
        self.color_menu.delete(0, tk.END)  # Clear the current color menu

        # Re-add predefined colors
        for index, color in enumerate(self.colors.colors):
            if index == self.colors.transperent_index:
                continue
            self.color_menu.add_radiobutton(label=rgb_to_hex(color), variable=self.color, value=index, background=rgb_to_hex(color), foreground=rgb_to_hex((255 - color[0], 255 - color[1], 255 - color[2])), selectcolor=rgb_to_hex((255 - color[0], 255 - color[1], 255 - color[2])))

    def undo_last_change(self):
        previous = self.buffer.undo()
        if previous is not None:
            self.pixel_indexes = previous
            self.refresh_canvas()
            self.edited = True

    def on_control_z(self, event):
        self.undo_last_change()

    def on_control_s(self, event):
        self.save_file()

    def on_n(self, event):
        self.brush.set('none')

    def on_b(self, event):
        self.brush.set('brush')

    def on_e(self, event):
        self.brush.set('erase')

    def on_f(self, event):
        self.brush.set('flood')

    def on_l(self, event):
        self.brush.set('line')

    def on_c(self, event):
        self.brush.set('circle')
